﻿using System;
using System.Collections.Generic;
using System.Data;
using Loki.Resources;
using Loki.Utils;

namespace Loki.Data
{
    /// <summary>
    /// Base class for entity builders.
    /// </summary>
    /// <typeparam name="TDataEntity">The type of the entity class.</typeparam>
    /// <typeparam name="TDataEntityInterface">The type of the data entity interface.</typeparam>
    /// <typeparam name="TDataEntityKey">The type of the entity reference.</typeparam>
    public abstract class EntityMapper<TDataEntity, TDataEntityInterface, TDataEntityKey> : CommandMapper
        where TDataEntity : TDataEntityInterface, IDataEntity<TDataEntityKey>
        where TDataEntityInterface : IDataEntity<TDataEntityKey>
    {
        public EntityMapper()
        {
            NewIDFromDatabase = false;
        }

        #region Properties set by spring
        public string GetRequest { get; set; }

        public string GetAllRequest { get; set; }

        public string InsertRequest { get; set; }

        public string UpdateRequest { get; set; }

        public string DeleteRequest { get; set; }

        public string SaveRequest { get; set; }

        public bool NewIDFromDatabase { get; set; }

        public string IDParameterName { get; set; }
        #endregion

        #region Loading process
        /// <summary>
        /// Loads the entity from data record.
        /// </summary>
        /// <param name="P_Entity">The entity.</param>
        /// <param name="P_Record">The data record.</param>
        public virtual void LoadFromDataRecord(TDataEntityInterface P_Entity, IDataRecord P_Record)
        {
            // Load ID
            try
            {
                P_Entity.PK = IDConverter(P_Record, IDParameterName);
            }
            catch (Exception Ex)
            {
                throw BuildErrorFormat<LokiException>(ErrorMessages.Data_Mappers_NoIdentifier, Ex, this.GetType().Name, IDParameterName);
            }

            // Load core
            try
            {
                LoadCoreData(P_Entity, P_Record);
            }
            catch (Exception Ex)
            {
                throw BuildErrorFormat<LokiException>(ErrorMessages.Data_Mappers_ReadError, Ex, this.GetType().Name, P_Entity.PK);
            }

            // Load sub objects
            LoadSubObjects(P_Entity, P_Record);
        }

        protected abstract Func<IDataRecord, string, TDataEntityKey> IDConverter { get; }

        /// <summary>
        /// Loads the core data.
        /// </summary>
        /// <param name="P_Entity">The entity.</param>
        /// <param name="P_Record">The record.</param>
        protected abstract void LoadCoreData(TDataEntityInterface P_Entity, IDataRecord P_Record);

        /// <summary>
        /// Loads the sub objects.
        /// </summary>
        /// <param name="P_Entity">The entity.</param>
        /// <param name="P_Record">The record.</param>
        protected virtual void LoadSubObjects(TDataEntityInterface P_Entity, IDataRecord P_Record)
        {
        }
        #endregion

        #region Factory
        /// <summary>
        /// Gets or sets the factory used for building entities during load.
        /// </summary>
        public IFactory<TDataEntity> Factory
        {
            get;
            protected set;
        }

        /// <summary>
        /// Gets or sets the discriminator field wich is passed to the factory.
        /// </summary>
        public string Discriminator
        {
            get;
            protected set;
        }
        #endregion

        #region General builders
        /// <summary>
        /// Builds one entity from data record.
        /// </summary>
        /// <param name="P_Record">The record.</param>
        /// <returns>Loaded entity.</returns>
        private TDataEntityInterface BuildOne(IDataReader P_Record)
        {
            // define factory creator
            Func<IDataReader, TDataEntityInterface> L_Creator = null;

            // select factory call
            if (string.IsNullOrEmpty(Discriminator))
            {
                L_Creator = (x) => Factory.Create(null);
            }
            else
            {
                L_Creator = (x) => Factory.Create(x[Discriminator]);
            }

            return CommandMapper.BuildOne<TDataEntityInterface>(P_Record, L_Creator, LoadFromDataRecord);
        }

        /// <summary>
        /// Builds an entity list from data reader.
        /// </summary>
        /// <param name="P_Reader">The reader.</param>
        /// <returns>List of loaded entities.</returns>
        private List<TDataEntityInterface> BuildList(IDataReader P_Reader)
        {
            // define factory creator
            Func<IDataReader, TDataEntityInterface> L_Creator = null;

            // select factory call
            if (string.IsNullOrEmpty(Discriminator))
            {
                L_Creator = (x) => Factory.Create(null);
            }
            else
            {
                L_Creator = (x) => Factory.Create(x[Discriminator]);
            }

            return CommandMapper.BuildList<TDataEntityInterface>(P_Reader, L_Creator, LoadFromDataRecord);
        }
        #endregion

        private const string REQUEST_TYPE_GET = "GET";
        private const string REQUEST_TYPE_GET_ALL = "GET ALL";
        private const string REQUEST_TYPE_GET_INSERT = "INSERT";
        private const string REQUEST_TYPE_GET_UPDATE = "UPDATE";
        private const string REQUEST_TYPE_GET_DELETE = "DELETE";

        public virtual TDataEntityInterface Get(IDbTransaction P_Transaction, TDataEntityKey P_Key)
        {
            if (string.IsNullOrEmpty(GetRequest))
            {
                throw BuildErrorFormat<ArgumentException>(ErrorMessages.Data_Mappers_NoRequestDefinied, REQUEST_TYPE_GET, GetType().Name);
            }

            return ExecuteGet(P_Transaction, GetRequest, c => c.AddParameter(IDParameterName, P_Key));
        }

        public virtual List<TDataEntityInterface> GetAll(IDbTransaction P_Transaction)
        {
            if (string.IsNullOrEmpty(GetAllRequest))
            {
                throw BuildErrorFormat<ArgumentException>(ErrorMessages.Data_Mappers_NoRequestDefinied, REQUEST_TYPE_GET_ALL, GetType().Name);
            }

            return ExecuteGetAll(P_Transaction, GetAllRequest, null);
        }

        public virtual TDataEntityInterface ExecuteGet(IDbTransaction P_Transaction, string P_CommandName, Action<IDatabaseCommand> P_MapParameters)
        {
            IDataReader L_CommandReturn = null;
            IDatabaseCommand L_Command = PrepareCommand(P_CommandName, P_MapParameters);
            try
            {
                if (LogQueries)
                {
                    Log.DebugFormat(LogStrings.Mapper_QueryExecute, L_Command.Dump());
                }

                L_CommandReturn = Database.ExecuteQuery(L_Command, P_Transaction);
                return BuildOne(L_CommandReturn);
            }
            catch (Exception Ex)
            {
                throw BuildErrorFormat<LokiException>(ErrorMessages.Data_Mappers_QueryError, Ex, this.GetType().Name, L_Command.Dump());
            }
        }

        public virtual List<TDataEntityInterface> ExecuteGetAll(IDbTransaction P_Transaction, string P_CommandName, Action<IDatabaseCommand> P_MapParameters)
        {
            IDataReader L_CommandReturn = null;
            IDatabaseCommand L_Command = PrepareCommand(P_CommandName, P_MapParameters);
            try
            {
                if (LogQueries)
                {
                    Log.DebugFormat(LogStrings.Mapper_QueryExecute, L_Command.Dump());
                }

                L_CommandReturn = Database.ExecuteQuery(L_Command, P_Transaction);
                return BuildList(L_CommandReturn);
            }
            catch (Exception Ex)
            {
                throw BuildErrorFormat<LokiException>(ErrorMessages.Data_Mappers_QueryError, Ex, this.GetType().Name, L_Command.Dump());
            }
        }

        public virtual void Delete(IDbTransaction P_Transaction, TDataEntityKey P_EntityKey)
        {
            if (string.IsNullOrEmpty(DeleteRequest))
            {
                throw BuildErrorFormat<ArgumentException>(ErrorMessages.Data_Mappers_NoRequestDefinied, REQUEST_TYPE_GET_DELETE, GetType().Name);
            }

            IDatabaseCommand L_Command = Database.CreateCommand(DeleteRequest);
            L_Command.AddParameter(IDParameterName, P_EntityKey);
            try
            {
                if (LogQueries)
                {
                    Log.DebugFormat(LogStrings.Mapper_QueryExecute, L_Command.Dump());
                }

                Database.ExecuteNonQuery(L_Command, P_Transaction);
            }
            catch (Exception Ex)
            {
                throw BuildErrorFormat<LokiException>(ErrorMessages.Data_Mappers_DeleteError, Ex, this.GetType().Name, P_EntityKey, L_Command.Dump());
            }
        }

        public virtual void Save(IDbTransaction P_Transaction, TDataEntityInterface P_Entity)
        {
            if (string.IsNullOrEmpty(SaveRequest))
            {
                TDataEntityInterface L_OldEntity = Get(P_Transaction, P_Entity.PK);
                if (object.Equals(L_OldEntity, default(TDataEntity)))
                {
                    ExecuteInsert(P_Transaction, P_Entity);
                }
                else
                {
                    ExecuteUpdate(P_Transaction, P_Entity);
                }
            }
            else
            {
                ExecuteSave(P_Transaction, P_Entity);
            }
        }

        private void ExecuteSave(IDbTransaction P_Transaction, TDataEntityInterface P_Entity)
        {
            IDatabaseCommand L_Command = Database.CreateCommand(SaveRequest);
            MapSaveParameters(L_Command, P_Entity);
            try
            {
                if (LogQueries)
                {
                    Log.DebugFormat(LogStrings.Mapper_QueryExecute, L_Command.Dump());
                }

                if (NewIDFromDatabase)
                {
                    P_Entity.PK = (TDataEntityKey)Database.ExecuteScalar(L_Command, P_Transaction);
                }
                else
                {
                    Database.ExecuteNonQuery(L_Command, P_Transaction);
                }
            }
            catch (Exception Ex)
            {
                throw BuildErrorFormat<LokiException>(ErrorMessages.Data_Mappers_SaveError, Ex, this.GetType().Name, P_Entity.PK, L_Command.Dump());
            }
        }

        private void ExecuteUpdate(IDbTransaction P_Transaction, TDataEntityInterface P_Entity)
        {
            if (string.IsNullOrEmpty(UpdateRequest))
            {
                throw BuildErrorFormat<ArgumentException>(ErrorMessages.Data_Mappers_NoRequestDefinied, REQUEST_TYPE_GET_UPDATE, GetType().Name);
            }

            IDatabaseCommand L_Command = Database.CreateCommand(UpdateRequest);
            MapSaveParameters(L_Command, P_Entity);
            try
            {
                if (LogQueries)
                {
                    Log.DebugFormat(LogStrings.Mapper_QueryExecute, L_Command.Dump());
                }

                Database.ExecuteNonQuery(L_Command, P_Transaction);
            }
            catch (Exception Ex)
            {
                throw BuildErrorFormat<LokiException>(ErrorMessages.Data_Mappers_SaveError, Ex, this.GetType().Name, P_Entity.PK, L_Command.Dump());
            }
        }

        private void ExecuteInsert(IDbTransaction P_Transaction, TDataEntityInterface P_Entity)
        {
            if (string.IsNullOrEmpty(InsertRequest))
            {
                throw BuildErrorFormat<ArgumentException>(ErrorMessages.Data_Mappers_NoRequestDefinied, REQUEST_TYPE_GET_INSERT, GetType().Name);
            }

            IDatabaseCommand L_Command = Database.CreateCommand(InsertRequest);
            MapSaveParameters(L_Command, P_Entity);

            try
            {
                if (LogQueries)
                {
                    Log.DebugFormat(LogStrings.Mapper_QueryExecute, L_Command.Dump());
                }

                if (NewIDFromDatabase)
                {
                    P_Entity.PK = (TDataEntityKey)Database.ExecuteScalar(L_Command, P_Transaction);
                }
                else
                {
                    Database.ExecuteNonQuery(L_Command, P_Transaction);
                }
            }
            catch (Exception Ex)
            {
                throw BuildErrorFormat<LokiException>(ErrorMessages.Data_Mappers_SaveError, Ex, this.GetType().Name, P_Entity.PK, L_Command.Dump());
            }
        }

        protected virtual void MapSaveParameters(IDatabaseCommand P_Command, TDataEntityInterface P_Entity)
        {
        }
    }
}
